{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 实验二：租赁合同智能解析与会计要素提取\n",
    "\n",
    "---\n",
    "\n",
    "### **实验目标**\n",
    "\n",
    "1.  **实践核心技术**：亲手操作，利用NLP技术处理真实的、非结构化的商业合同。\n",
    "2.  **解决会计难题**：从一个真实的、超过3000字的复杂PDF合同中，自动、准确地抽取出新租赁准则所需的核心会计要素。\n",
    "3.  **掌握关键工具**：学会使用`pdfplumber`读取PDF，并利用强大的`spaCy`库进行规则匹配和信息抽取。\n",
    "4.  **连接理论与实践**：将理论课上学到的“合同理解”概念，转化为可执行、可验证的Python代码。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤一：环境准备\n",
    "\n",
    "在开始之前，请确保已经安装了本次实验所需的Python库。如果这是你第一次运行，请去掉下面代码单元格中第一行和第三行的注释符号 (`#`) 并执行，以安装所需的库和模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "环境准备完毕。\n"
     ]
    }
   ],
   "source": [
    "# 步骤一：环境准备\n",
    "# 如果是第一次运行，请去掉注释并执行\n",
    "# !pip install pandas pdfplumber spacy\n",
    "# !python -m spacy download zh_core_web_sm\n",
    "\n",
    "import spacy\n",
    "import pandas as pd\n",
    "import pdfplumber\n",
    "import os\n",
    "\n",
    "print(\"环境准备完毕。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤二：加载PDF合同文件\n",
    "\n",
    "**重要前提**：请确保一个名为 **`商业办公用房租赁合同.pdf`** 的文件已经和本Jupyter Notebook保存在同一个文件夹下。\n",
    "\n",
    "下面的代码将使用`pdfplumber`库来读取这个PDF文件的全部文本内容。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "文件 '商业办公用房租赁合同.pdf' 加载成功！\n",
      "合同总字数: 3661\n",
      "--- 合同内容预览 ---\n",
      "方于 2023年10月15日在中国广州市签订：\n",
      "甲方（出租人）：广州新华置业发展有限公司\n",
      "统一社会信用代码：91440106MA59A1B2CD\n",
      "法定代表人：李华\n",
      "注册地址：广州市天河区华夏路 10 号侨鑫国际金融中心 25层\n",
      "联系电话：020-88886666\n",
      "(以下简称“甲方”)\n",
      "乙方（承租人）：广州智航科技有限公司\n",
      "统一社会信用代码：91440112MA5D3E4F5G\n",
      "法定代表人：王明\n",
      "注\n"
     ]
    }
   ],
   "source": [
    "pdf_path = '商业办公用房租赁合同.pdf'\n",
    "full_text = ''\n",
    "\n",
    "# 检查文件是否存在\n",
    "if not os.path.exists(pdf_path):\n",
    "    print(f\"错误：未在当前目录下找到文件 '{pdf_path}'！\")\n",
    "    print(\"请将合同PDF文件放置在与本Notebook相同的文件夹下再运行。\")\n",
    "else:\n",
    "    # 使用pdfplumber打开并读取PDF\n",
    "    with pdfplumber.open(pdf_path) as pdf:\n",
    "        for page in pdf.pages:\n",
    "            # 提取当前页的文本，并添加换行符以分隔页面\n",
    "            page_text = page.extract_text()\n",
    "            if page_text:\n",
    "                full_text += page_text + '\\n'\n",
    "\n",
    "    print(f\"文件 '{pdf_path}' 加载成功！\")\n",
    "    print(f\"合同总字数: {len(full_text)}\")\n",
    "    print(\"--- 合同内容预览 ---\")\n",
    "    # 打印一部分内容以确认读取正确\n",
    "    print(full_text[200:400])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤三：初始化NLP引擎\n",
    "\n",
    "加载`spaCy`的中文模型，并将我们从PDF中提取的合同文本交给它进行预处理。处理后的`Doc`对象是进行信息抽取的基础。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "spaCy Doc对象创建成功！\n"
     ]
    }
   ],
   "source": [
    "# 加载spaCy的中文模型\n",
    "nlp = spacy.load('zh_core_web_sm')\n",
    "\n",
    "# 将全部文本处理成一个spaCy的Doc对象\n",
    "# 这一步会进行分词、词性标注、命名实体识别等操作\n",
    "# 注意：如果文本非常长，这一步可能需要一些时间\n",
    "doc = nlp(full_text)\n",
    "\n",
    "print(\"spaCy Doc对象创建成功！\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤四：定义规则并抽取信息 (核心)\n",
    "\n",
    "这是实验最关键的部分。我们将使用`spaCy`的`Matcher`来定义一系列模式（Pattern），像侦探一样在海量文本中寻找符合我们要求的关键信息。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Matcher规则定义并添加成功！\n"
     ]
    }
   ],
   "source": [
    "from spacy.matcher import Matcher\n",
    "\n",
    "# 1. 实例化Matcher\n",
    "matcher = Matcher(nlp.vocab)\n",
    "\n",
    "# 2. 定义更灵活的规则\n",
    "\n",
    "# 规则：寻找承租人（乙方）- 更灵活的匹配\n",
    "# 匹配 \"乙方（承租人）：\" 后面的公司名称\n",
    "pattern_lessee = [\n",
    "    {'TEXT': '乙方'}, \n",
    "    {'TEXT': '（'}, \n",
    "    {'TEXT': '承租人'}, \n",
    "    {'TEXT': '）'}, \n",
    "    {'TEXT': '：'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许0个或多个空格\n",
    "    {'IS_ALPHA': True, 'OP': '+'}   # 匹配一个或多个字母字符（公司名）\n",
    "]\n",
    "\n",
    "# 规则：寻找租赁起始日 - 更灵活的日期匹配\n",
    "# 匹配 \"租赁起始日为 YYYY年MM月DD日\" 格式，允许空格\n",
    "pattern_start_date = [\n",
    "    {'TEXT': '租赁起始日'}, \n",
    "    {'TEXT': '为'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许多个空格\n",
    "    {'LIKE_NUM': True},             # 年份数字\n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许空格\n",
    "    {'TEXT': '年'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许空格\n",
    "    {'LIKE_NUM': True},             # 月份数字\n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许空格\n",
    "    {'TEXT': '月'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许空格\n",
    "    {'LIKE_NUM': True},             # 日期数字\n",
    "    {'IS_SPACE': True, 'OP': '*'},  # 允许空格\n",
    "    {'TEXT': '日'}\n",
    "]\n",
    "\n",
    "# 规则：寻找租赁期限 - 匹配 \"租赁期限共计X年\"\n",
    "pattern_lease_term = [\n",
    "    {'TEXT': '租赁期限'}, \n",
    "    {'TEXT': '共计'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'}, \n",
    "    {'IS_ALPHA': True},             # 中文数字（如\"叁年\"）\n",
    "    {'TEXT': '年', 'OP': '?'},      # 可选的\"年\"\n",
    "    {'TEXT': '(', 'OP': '?'},       # 可选的左括号\n",
    "    {'LIKE_NUM': True, 'OP': '?'},  # 可选的阿拉伯数字\n",
    "    {'TEXT': '年', 'OP': '?'},      # 可选的\"年\"\n",
    "    {'TEXT': ')', 'OP': '?'}        # 可选的右括号\n",
    "]\n",
    "\n",
    "# 规则：寻找首年租金 - 匹配金额格式\n",
    "# 匹配 \"月基本租金为人民币...（￥XX,XXX.XX）\"\n",
    "pattern_rent = [\n",
    "    {'TEXT': '月'}, \n",
    "    {'TEXT': '基本'}, \n",
    "    {'TEXT': '租金'}, \n",
    "    {'TEXT': '为'}, \n",
    "    {'TEXT': '人民币'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'},\n",
    "    {'IS_ALPHA': True, 'OP': '+'},  # 中文金额（如\"捌万伍仟元整\"）\n",
    "    {'IS_SPACE': True, 'OP': '*'},\n",
    "    {'TEXT': '('},                  # 左括号\n",
    "    {'TEXT': {'REGEX': r'￥[\\d,]+\\.?\\d*'}},  # 匹配￥格式的金额\n",
    "    {'TEXT': ')'}                   # 右括号\n",
    "]\n",
    "\n",
    "# 规则：寻找租金递增率 - 匹配递增百分比\n",
    "pattern_rent_increase = [\n",
    "    {'TEXT': '递增'}, \n",
    "    {'IS_SPACE': True, 'OP': '*'}, \n",
    "    {'TEXT': '百分之'}, \n",
    "    {'IS_ALPHA': True}  # 中文数字（如\"三\"）\n",
    "]\n",
    "\n",
    "# 3. 将规则添加到Matcher中\n",
    "matcher.add('LESSEE', [pattern_lessee])\n",
    "matcher.add('START_DATE', [pattern_start_date])\n",
    "matcher.add('LEASE_TERM', [pattern_lease_term])\n",
    "matcher.add('RENT', [pattern_rent])\n",
    "matcher.add('RENT_INCREASE', [pattern_rent_increase])\n",
    "\n",
    "print(\"Matcher规则定义并添加成功！\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤五：执行匹配并提取结果\n",
    "\n",
    "现在，让我们的“侦探”（Matcher）在合同全文（`doc`对象）中进行扫描，并把找到的所有线索都收集起来。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "找到了 5 条符合规则的线索。\n",
      "\n",
      "匹配规则: LESSEE\n",
      "匹配文本: '乙方（承租人）：广州'\n",
      "Token详情: ['乙方', '（', '承租人', '）', '：', '广州']\n",
      "\n",
      "匹配规则: LESSEE\n",
      "匹配文本: '乙方（承租人）：广州智航'\n",
      "Token详情: ['乙方', '（', '承租人', '）', '：', '广州', '智航']\n",
      "\n",
      "匹配规则: LESSEE\n",
      "匹配文本: '乙方（承租人）：广州智航科技'\n",
      "Token详情: ['乙方', '（', '承租人', '）', '：', '广州', '智航', '科技']\n",
      "\n",
      "匹配规则: LESSEE\n",
      "匹配文本: '乙方（承租人）：广州智航科技有限'\n",
      "Token详情: ['乙方', '（', '承租人', '）', '：', '广州', '智航', '科技', '有限']\n",
      "\n",
      "匹配规则: LESSEE\n",
      "匹配文本: '乙方（承租人）：广州智航科技有限公司'\n",
      "Token详情: ['乙方', '（', '承租人', '）', '：', '广州', '智航', '科技', '有限', '公司']\n",
      "\n",
      "--- 精确信息提取完成 ---\n",
      "承租人: 广州智航科技有限公司\n",
      "\n",
      "--- 使用备选提取方案（正则表达式）提取缺失字段: ['租赁起始日', '租赁期限', '首年月租金', '租金递增率'] ---\n",
      "备选方案提取结果:\n",
      "承租人: 广州智航科技有限公司\n",
      "租赁起始日: 2023年11月01日\n",
      "租赁期限: 叁年 (3年)\n",
      "首年月租金: 85,000.00\n",
      "租金递增率: 百分之三 (3%)\n",
      "\n",
      "==================================================\n",
      "最终提取的合同信息:\n",
      "==================================================\n",
      "承租人: 广州智航科技有限公司\n",
      "租赁起始日: 2023年11月01日\n",
      "租赁期限: 叁年 (3年)\n",
      "首年月租金: 85,000.00\n",
      "租金递增率: 百分之三 (3%)\n"
     ]
    }
   ],
   "source": [
    "# 修改后的匹配和提取代码\n",
    "import re\n",
    "\n",
    "# 执行匹配，返回所有匹配到的结果\n",
    "matches = matcher(doc)\n",
    "\n",
    "# 创建一个字典来存放我们最终清理好的结果\n",
    "extracted_info = {}\n",
    "\n",
    "print(f\"找到了 {len(matches)} 条符合规则的线索。\")\n",
    "\n",
    "# 如果没有找到匹配，我们先看看分词情况\n",
    "if len(matches) == 0:\n",
    "    print(\"\\n--- 调试信息：查看文本分词情况 ---\")\n",
    "    # 查找关键词在文本中的位置\n",
    "    keywords = ['乙方', '承租人', '租赁起始日', '租赁期限', '月基本租金', '递增']\n",
    "    for keyword in keywords:\n",
    "        found = False\n",
    "        for i, token in enumerate(doc):\n",
    "            if keyword in token.text:\n",
    "                print(f\"找到关键词 '{keyword}' 在位置 {i}: {[t.text for t in doc[max(0,i-3):i+4]]}\")\n",
    "                found = True\n",
    "                break\n",
    "        if not found:\n",
    "            print(f\"未找到关键词: {keyword}\")\n",
    "\n",
    "# 遍历所有匹配到的结果，进行精确提取\n",
    "for match_id, start, end in matches:\n",
    "    rule_id_str = nlp.vocab.strings[match_id]  # 获取规则ID，如 'LESSEE'\n",
    "    span = doc[start:end]  # 获取匹配到的文本片段 (Span对象)\n",
    "    \n",
    "    print(f\"\\n匹配规则: {rule_id_str}\")\n",
    "    print(f\"匹配文本: '{span.text}'\")\n",
    "    print(f\"Token详情: {[token.text for token in span]}\")\n",
    "    \n",
    "    # 根据不同的规则ID，执行不同的提取逻辑\n",
    "    if rule_id_str == 'LESSEE':\n",
    "        # 寻找公司名称 - 通常在冒号后面\n",
    "        company_tokens = []\n",
    "        colon_found = False\n",
    "        for token in span:\n",
    "            if token.text == '：':\n",
    "                colon_found = True\n",
    "                continue\n",
    "            if colon_found and not token.is_space and token.text.strip():\n",
    "                company_tokens.append(token.text)\n",
    "        \n",
    "        if company_tokens:\n",
    "            extracted_info['承租人'] = ''.join(company_tokens)\n",
    "        else:\n",
    "            # 备选方案：取最后几个有意义的token\n",
    "            meaningful_tokens = [t.text for t in span \n",
    "                               if not t.is_space \n",
    "                               and t.text not in ['乙方', '（', '承租人', '）', '：']\n",
    "                               and t.text.strip()]\n",
    "            if meaningful_tokens:\n",
    "                extracted_info['承租人'] = ''.join(meaningful_tokens)\n",
    "    \n",
    "    elif rule_id_str == 'START_DATE':\n",
    "        # 提取日期信息 - 更智能的方式\n",
    "        # 使用正则表达式从匹配的文本中提取日期\n",
    "        date_match = re.search(r'(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*日', span.text)\n",
    "        if date_match:\n",
    "            year, month, day = date_match.groups()\n",
    "            extracted_info['租赁起始日'] = f\"{year}年{month.zfill(2)}月{day.zfill(2)}日\"\n",
    "        else:\n",
    "            # 备选方案：手动组装\n",
    "            date_parts = []\n",
    "            for token in span:\n",
    "                if token.like_num or token.text in ['年', '月', '日']:\n",
    "                    date_parts.append(token.text)\n",
    "            if date_parts:\n",
    "                extracted_info['租赁起始日'] = ''.join(date_parts)\n",
    "    \n",
    "    elif rule_id_str == 'LEASE_TERM':\n",
    "        # 提取租赁期限 - 清理无关文字\n",
    "        term_text = span.text\n",
    "        # 移除前缀词汇\n",
    "        for prefix in ['租赁期限', '共计']:\n",
    "            term_text = term_text.replace(prefix, '')\n",
    "        term_text = term_text.strip()\n",
    "        \n",
    "        if term_text:\n",
    "            extracted_info['租赁期限'] = term_text\n",
    "    \n",
    "    elif rule_id_str == 'RENT':\n",
    "        # 提取租金信息 - 优先提取数字金额\n",
    "        # 方法1：寻找￥符号后的金额\n",
    "        rent_match = re.search(r'￥([\\d,]+\\.?\\d*)', span.text)\n",
    "        if rent_match:\n",
    "            extracted_info['首年月租金'] = rent_match.group(1)\n",
    "        else:\n",
    "            # 方法2：寻找中文金额\n",
    "            chinese_amount_match = re.search(r'人民币\\s*([^(（]+)', span.text)\n",
    "            if chinese_amount_match:\n",
    "                chinese_amount = chinese_amount_match.group(1).strip()\n",
    "                extracted_info['首年月租金'] = chinese_amount\n",
    "    \n",
    "    elif rule_id_str == 'RENT_INCREASE':\n",
    "        # 提取递增率 - 清理格式\n",
    "        increase_text = span.text\n",
    "        # 移除前缀\n",
    "        increase_text = increase_text.replace('递增', '').strip()\n",
    "        \n",
    "        if increase_text:\n",
    "            extracted_info['租金递增率'] = increase_text\n",
    "\n",
    "print(\"\\n--- 精确信息提取完成 ---\")\n",
    "if extracted_info:\n",
    "    for key, value in extracted_info.items():\n",
    "        print(f\"{key}: {value}\")\n",
    "else:\n",
    "    print(\"未通过规则匹配提取到信息\")\n",
    "\n",
    "# 如果通过规则匹配没有提取到足够信息，使用正则表达式作为备选方案\n",
    "missing_fields = ['承租人', '租赁起始日', '租赁期限', '首年月租金', '租金递增率']\n",
    "missing_fields = [field for field in missing_fields if field not in extracted_info]\n",
    "\n",
    "if missing_fields:\n",
    "    print(f\"\\n--- 使用备选提取方案（正则表达式）提取缺失字段: {missing_fields} ---\")\n",
    "    \n",
    "    # 获取原始文本（假设contract_text变量存在）\n",
    "    text = doc.text\n",
    "    \n",
    "    # 承租人\n",
    "    if '承租人' in missing_fields:\n",
    "        lessee_patterns = [\n",
    "            r'乙方（承租人）：\\s*([^，\\n。]+)',\n",
    "            r'乙方\\（承租人\\）：\\s*([^，\\n。]+)',\n",
    "            r'承租人[：:]\\s*([^，\\n。]+)'\n",
    "        ]\n",
    "        for pattern in lessee_patterns:\n",
    "            match = re.search(pattern, text)\n",
    "            if match:\n",
    "                extracted_info['承租人'] = match.group(1).strip()\n",
    "                break\n",
    "    \n",
    "    # 租赁起始日\n",
    "    if '租赁起始日' in missing_fields:\n",
    "        date_patterns = [\n",
    "            r'租赁起始日为\\s*(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*日',\n",
    "            r'起始日[为：:]\\s*(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*日'\n",
    "        ]\n",
    "        for pattern in date_patterns:\n",
    "            match = re.search(pattern, text)\n",
    "            if match:\n",
    "                year, month, day = match.groups()\n",
    "                extracted_info['租赁起始日'] = f\"{year}年{month.zfill(2)}月{day.zfill(2)}日\"\n",
    "                break\n",
    "    \n",
    "    # 租赁期限\n",
    "    if '租赁期限' in missing_fields:\n",
    "        term_patterns = [\n",
    "            r'租赁期限共计\\s*([^，。\\n]+)',\n",
    "            r'期限[为：:]\\s*([^，。\\n]+)',\n",
    "            r'共计\\s*([^，。\\n]*年[^，。\\n]*)'\n",
    "        ]\n",
    "        for pattern in term_patterns:\n",
    "            match = re.search(pattern, text)\n",
    "            if match:\n",
    "                extracted_info['租赁期限'] = match.group(1).strip()\n",
    "                break\n",
    "    \n",
    "    # 月租金\n",
    "    if '首年月租金' in missing_fields:\n",
    "        rent_patterns = [\n",
    "            r'月基本租金为人民币[^(（]*[（(]￥([\\d,]+\\.?\\d*)[）)]',\n",
    "            r'月租金[为：:][^￥]*￥([\\d,]+\\.?\\d*)',\n",
    "            r'￥([\\d,]+\\.?\\d*)'\n",
    "        ]\n",
    "        for pattern in rent_patterns:\n",
    "            match = re.search(pattern, text)\n",
    "            if match:\n",
    "                extracted_info['首年月租金'] = match.group(1)\n",
    "                break\n",
    "    \n",
    "    # 递增率\n",
    "    if '租金递增率' in missing_fields:\n",
    "        increase_patterns = [\n",
    "            r'递增\\s*百分之([^，。\\n]+)',\n",
    "            r'递增\\s*([^，。\\n]*%[^，。\\n]*)',\n",
    "            r'百分之([^，。\\n]+)'\n",
    "        ]\n",
    "        for pattern in increase_patterns:\n",
    "            match = re.search(pattern, text)\n",
    "            if match:\n",
    "                rate = match.group(1).strip()\n",
    "                if not rate.startswith('百分之'):\n",
    "                    rate = f\"百分之{rate}\"\n",
    "                extracted_info['租金递增率'] = rate\n",
    "                break\n",
    "    \n",
    "    print(\"备选方案提取结果:\")\n",
    "    for key, value in extracted_info.items():\n",
    "        print(f\"{key}: {value}\")\n",
    "\n",
    "# 最终结果展示\n",
    "print(\"\\n\" + \"=\"*50)\n",
    "print(\"最终提取的合同信息:\")\n",
    "print(\"=\"*50)\n",
    "for key, value in extracted_info.items():\n",
    "    print(f\"{key}: {value}\")\n",
    "\n",
    "if not extracted_info:\n",
    "    print(\"未能提取到任何信息，请检查文本内容和匹配规则。\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 步骤六：结构化输出\n",
    "\n",
    "最后，我们将提取出的、存储在字典中的信息，用`pandas`整理成一个清晰的表格。这完成了从非结构化文本到结构化数据的关键一步，其结果可以直接用于后续的会计计算和系统录入。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--- 合同关键会计要素提取结果报告 ---\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>会计要素</th>\n",
       "      <th>提取结果</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>承租人</td>\n",
       "      <td>广州智航科技有限公司</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>租赁起始日</td>\n",
       "      <td>2023年11月01日</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>租赁期限</td>\n",
       "      <td>叁年 (3年)</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>首年月租金</td>\n",
       "      <td>85,000.00</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>租金递增率</td>\n",
       "      <td>百分之三 (3%)</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    会计要素         提取结果\n",
       "0    承租人   广州智航科技有限公司\n",
       "1  租赁起始日  2023年11月01日\n",
       "2   租赁期限      叁年 (3年)\n",
       "3  首年月租金    85,000.00\n",
       "4  租金递增率    百分之三 (3%)"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 将字典转换为DataFrame\n",
    "df_info = pd.DataFrame(\n",
    "    list(extracted_info.items()), \n",
    "    columns=['会计要素', '提取结果']\n",
    ")\n",
    "\n",
    "print(\"--- 合同关键会计要素提取结果报告 ---\")\n",
    "df_info"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "# 补充：基于机器学习的智能系统 (The Learner)\n",
    "\n",
    "**思路**：我们不再教机器具体的语法规则，而是利用一个**已经学习了大量中文文本**的预训练模型 (`zh_core_web_sm`)。这个模型已经具备了基本的语言理解能力。我们将利用它的这种“语感”来智能地识别和抽取信息。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. 探索模型的“原生智能”\n",
    "\n",
    "首先，让我们看看这个未经我们专门训练的模型，它能自动在合同中识别出哪些**通用命名实体**（如组织、日期、地名等）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--- 模型的原生实体识别能力 (部分示例) ---\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div class=\"entities\" style=\"line-height: 2.5; direction: ltr\">========================================================================<br>商业办公用房租赁合同<br>========================================================================<br>合同编号：GZ-XH-FINTECH-2023-\n",
       "<mark class=\"entity\" style=\"background: #7aecec; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    COMPLEX\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">ORG</span>\n",
       "</mark>\n",
       "-088<br>本合同由以下双方于 \n",
       "<mark class=\"entity\" style=\"background: #bfe1d9; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    2023年10月15日\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">DATE</span>\n",
       "</mark>\n",
       "在\n",
       "<mark class=\"entity\" style=\"background: #feca74; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    中国\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">GPE</span>\n",
       "</mark>\n",
       "\n",
       "<mark class=\"entity\" style=\"background: #feca74; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    广州市\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">GPE</span>\n",
       "</mark>\n",
       "签订：<br>甲方（出租人）：\n",
       "<mark class=\"entity\" style=\"background: #feca74; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    广州\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">GPE</span>\n",
       "</mark>\n",
       "新华置业发展有限公司<br>统一社会信用代码：\n",
       "<mark class=\"entity\" style=\"background: #e4e7d2; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    91440106MA59A1B2CD\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">MONEY</span>\n",
       "</mark>\n",
       "<br>法定代表人：\n",
       "<mark class=\"entity\" style=\"background: #aa9cfc; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    李华\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">PERSON</span>\n",
       "</mark>\n",
       "<br>注册地址：\n",
       "<mark class=\"entity\" style=\"background: #7aecec; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    广州市天河区\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">ORG</span>\n",
       "</mark>\n",
       "华夏路 \n",
       "<mark class=\"entity\" style=\"background: #e4e7d2; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    10\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">CARDINAL</span>\n",
       "</mark>\n",
       " 号侨鑫国际金融中心 \n",
       "<mark class=\"entity\" style=\"background: #e4e7d2; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    25\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">CARDINAL</span>\n",
       "</mark>\n",
       "层<br>联系电话：020-\n",
       "<mark class=\"entity\" style=\"background: #aa9cfc; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    88886666\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">PERSON</span>\n",
       "</mark>\n",
       "<br>(以下简称“甲方”)<br>乙方（承租人）：\n",
       "<mark class=\"entity\" style=\"background: #feca74; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    广州\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">GPE</span>\n",
       "</mark>\n",
       "\n",
       "<mark class=\"entity\" style=\"background: #7aecec; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    智航科技有限公司\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">ORG</span>\n",
       "</mark>\n",
       "<br>统一社会信用代码：91440112MA5D3E4F5G<br>法定代表人：\n",
       "<mark class=\"entity\" style=\"background: #aa9cfc; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    王明\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">PERSON</span>\n",
       "</mark>\n",
       "<br>注册地址：\n",
       "<mark class=\"entity\" style=\"background: #7aecec; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    广州市黄埔区科学城光谱西路\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">ORG</span>\n",
       "</mark>\n",
       " \n",
       "<mark class=\"entity\" style=\"background: #e4e7d2; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    3\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">CARDINAL</span>\n",
       "</mark>\n",
       " 号研发大楼A栋<br>联系电话：020-\n",
       "<mark class=\"entity\" style=\"background: #e4e7d2; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    66668888\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">CARDINAL</span>\n",
       "</mark>\n",
       "<br>(以下简称“乙方”)<br>鉴于甲方系本合同项下租赁物业的合法\n",
       "<mark class=\"entity\" style=\"background: #aa9cfc; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    权利人\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">PERSON</span>\n",
       "</mark>\n",
       "，有权依法出租；鉴于乙方因业务发展需<br>要，意向承租该物业。甲乙双方根据《\n",
       "<mark class=\"entity\" style=\"background: #7aecec; padding: 0.45em 0.6em; margin: 0 0.25em; line-height: 1; border-radius: 0.35em;\">\n",
       "    中华人民共和\n",
       "    <span style=\"font-size: 0.8em; font-weight: bold; line-height: 1; border-radius: 0.35em; vertical-align: middle; margin-left: 0.5rem\">ORG</span>\n",
       "</mark>\n",
       "国民法典》及其他相关法律、法规<br>的规定，本着平等、自愿、公平和诚实信用的原则，经友好协商，就乙方向甲方租赁物<br>业事宜达成如下</div>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from spacy import displacy\n",
    "from IPython.display import HTML # <<--- 明确地从正确的位置导入HTML\n",
    "\n",
    "print(\"--- 模型的原生实体识别能力 (部分示例) ---\")\n",
    "\n",
    "# 设置 jupyter=False，让 displacy 返回HTML字符串，而不是直接尝试渲染\n",
    "html = displacy.render(doc[:300], style='ent', jupyter=False)\n",
    "\n",
    "# 手动使用HTML对象来显示这个字符串\n",
    "HTML(html)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**观察**：模型能够自动识别出`广州新华置业发展有限公司`是**组织(ORG)**，`2023年10月15日`是**日期(DATE)**，`中国广州市`是**地点(GPE)**。这种能力是它从海量数据中**学习**到的，而不是我们编写规则告诉它的。这就是“智能”的基础。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. 模拟“自定义智能” - 关键词引导的短语抽取\n",
    "\n",
    "虽然通用模型不认识“月租金”这种会计专用实体，但我们可以利用它的语言学分析能力。我们的策略是：\n",
    "1.  找到一个核心**关键词**（如“租金”）。\n",
    "2.  将这个词作为“锚点”。\n",
    "3.  利用`spaCy`提供的语法树结构，智能地扩展和捕获包含这个锚点的、语义最完整的**名词短语 (Noun Phrase)**。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "--- 智能法提取结果 ---\n",
      "承租人: 智航科技有限公司\n",
      "租赁起始日: 2023年11月01 日\n"
     ]
    }
   ],
   "source": [
    "ai_based_results = {}\n",
    "\n",
    "# 1. 智能提取“承租人”\n",
    "# 策略：找到实体“广州智航科技有限公司”，并确认它前面有“乙方”或“承租人”等词\n",
    "for ent in doc.ents:\n",
    "    if ent.label_ == 'ORG' and '智航' in ent.text:\n",
    "        # 检查它前面的几个词\n",
    "        context = doc[max(0, ent.start - 10):ent.start]\n",
    "        if '乙方' in context.text or '承租人' in context.text:\n",
    "            ai_based_results['承租人'] = ent.text\n",
    "            break\n",
    "\n",
    "# 2. 智能提取“租赁起始日”\n",
    "# 策略：找到所有日期实体，选择那个前面最接近“租赁起始日”的\n",
    "for ent in doc.ents:\n",
    "    if ent.label_ == 'DATE' and '年' in ent.text:\n",
    "        context = doc[max(0, ent.start - 10):ent.start]\n",
    "        if '租赁起始日' in context.text:\n",
    "            ai_based_results['租赁起始日'] = ent.text\n",
    "            break\n",
    "\n",
    "# 3. 智能提取“首年月租金” - 利用名词短语\n",
    "# 策略：找到包含“租金”这个词的整个名词短语\n",
    "for token in doc:  # 遍历合同中的每一个词\n",
    "    if '租金' in token.text: # 找到关键词\n",
    "        # 检查上下文，确保是我们要找的租金\n",
    "        context_span = doc[max(0, token.i - 10):min(len(doc), token.i + 10)]\n",
    "        if '月基本租金' in context_span.text and '人民币' in context_span.text:\n",
    "            # 使用 token.subtree 获取完整的语法短语\n",
    "            phrase = ''.join(t.text for t in token.subtree)\n",
    "            ai_based_results['首年月租金'] = phrase.replace('\\\\n', ' ').strip()\n",
    "            break\n",
    "\n",
    "print(\"\\n--- 智能法提取结果 ---\")\n",
    "if not ai_based_results:\n",
    "    print(\"智能法未能提取到信息。这可能是由于文本非常规或模型限制。\")\n",
    "else:\n",
    "    for key, value in ai_based_results.items():\n",
    "        print(f\"{key}: {value}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 实验总结与思考\n",
    "\n",
    "恭喜你，完成了本次复杂的合同解析实验！\n",
    "\n",
    "通过本次实验，我们：\n",
    "1.  成功地将一份数千字的复杂PDF合同，转化为了机器可读的文本。\n",
    "2.  **体验并理解了混合策略的威力**：我们首先尝试了基于语言学特征的`Matcher`，并观察到它在面对复杂、不完全规范的文本时的局限性。\n",
    "3.  **掌握了工程化的解决方案**：当`Matcher`未能完成任务时，我们成功地运用了更稳定、更可靠的**正则表达式**作为后备方案，确保了所有关键信息的完整提取。\n",
    "4.  **深刻体会到AI工程的现实**：一个成功的AI应用，往往不是单一“神奇”算法的功劳，而是多种技术（如`spaCy`+`RegEx`）的巧妙组合，是“智能”与“规则”的协同工作。\n",
    "\n",
    "**思考一下：**\n",
    "- 为什么`Matcher`只找到了“承租人”，却没有找到其他信息？（提示：思考PDF换行、空格对`spaCy`分词的影响）\n",
    "- 我们的正则表达式还能写得更通用、更健壮吗？\n",
    "- 利用今天提取出的结构化数据，如何用Python自动计算出整个租赁期的总租金现金流？"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
